﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Pfz.RemoteGaming.Internal;
using Pfz.Threading;

namespace Pfz.RemoteGaming
{
	/// <summary>
	/// This class represents a Room in the server game.
	/// Changes to remote game properties are seen by everyone that is in the same room.
	/// Usually games start with exclusive rooms (so only client and server comunicate) and then
	/// the Participant joins another (common) room.
	/// </summary>
	public class RemoteGameRoom:
		ThreadSafeExceptionAwareDisposable
	{
		#region Fields
			internal static long _idGenerator;
			internal HashSet<RemoteGameComponent> _addedComponents = new HashSet<RemoteGameComponent>();
			internal HashSet<RemoteGameComponent> _removedComponents = new HashSet<RemoteGameComponent>();
			internal Dictionary<long, RemoteGameComponent> _components = new Dictionary<long, RemoteGameComponent>();
			internal bool _started;
			private IEnumerator<bool> _animation;
			private HighPrecisionTimer _timer;
			internal static long _participantIdGenerator;
		#endregion

		#region Constructor
			/// <summary>
			/// Creates a new room.
			/// </summary>
			public RemoteGameRoom()
			{
				MustDisposeIfEmpty = true;
			}
		#endregion
		#region Dispose
			/// <summary>
			/// Disposes the actual room.
			/// </summary>
			protected override void Dispose(bool disposing)
			{
				if (disposing)
				{
					Disposer.Dispose(ref _timer);
					Disposer.Dispose(ref _animation);

					var components = _components;
					if (components != null)
					{
						foreach(var component in components.Values)
							component.Dispose();

						_components = null;
					}

					_addedComponents = null;
					_removedComponents = null;
				}

				base.Dispose(disposing);
			}
		#endregion

		#region Properties
			#region _RoomLock
				internal object _RoomLock
				{
					get
					{
						return base.DisposeLock;
					}
				}
			#endregion

			#region MustDisposeIfEmpty
				/// <summary>
				/// Gets or sets a value telling if the room must be disposed if there are no Participants in it.
				/// The default is true.
				/// </summary>
				public bool MustDisposeIfEmpty { get; set; }
			#endregion
			#region ParticipantCount
				/// <summary>
				/// Gets the number of participants in this room.
				/// </summary>
				public int ParticipantCount
				{
					get
					{
						lock(DisposeLock)
						{
							CheckUndisposed();

							return _participants.Count;
						}
					}
				}
			#endregion
			#region UpdateTime
				/// <summary>
				/// Gets or sets the interval time used between frames.
				/// A value of 0 means that there will be no updates.
				/// </summary>
				public TimeSpan UpdateTime
				{
					get
					{
						var timer = _timer;
						if (timer == null)
							return TimeSpan.Zero;

						return timer.Interval;
					}
				}
			#endregion
		#endregion
		#region Methods
			#region CheckUndisposed
				internal new void CheckUndisposed()
				{
					base.CheckUndisposed();
				}
			#endregion

			#region _CreateComponent
				internal T _CreateComponent<T>(RemoteGameParticipant owner)
				where
					T: RemoteGameComponent
				{
					var result = (T)_AssemblyGenerator.GetConstructor(typeof(T)).Invoke(null);
					try
					{
						result._roomLock = DisposeLock;
						lock(_RoomLock)
						{
							CheckUndisposed();
							result._room = this;
							long id = Interlocked.Increment(ref _idGenerator);
							result._id = id;
							_addedComponents.Add(result);
							_components.Add(id, result);
							result.IsPublic = true;

							if (owner != null)
								result.Owner = owner;

							result.OnInitialize();
						}
					}
					catch
					{
						result.Dispose();
						throw;
					}

					return result;
				}
			#endregion
			#region _RemoveComponent
				internal void _RemoveComponent(RemoteGameComponent component)
				{
					lock(_RoomLock)
					{
						if (WasDisposed)
							return;

						if (!_addedComponents.Remove(component))
							_removedComponents.Add(component);

						_components.Remove(component._id);
					}
				}
			#endregion
			#region _ApplyChangesImmediately
				private void _ApplyChangesImmediately()
				{
					try
					{
						_ApplyChangesImmediately(true);
					}
					catch(Exception exception)
					{
						Dispose(exception);
					}
				}
				private void _ApplyChangesImmediately(bool updateAnimation)
				{
					RemoteGameParticipant[] participants;
					RemoteGameComponent[] components;
					RemoteGameComponent[] addedComponents;
					RemoteGameComponent[] removedComponents;
					IEnumerator<bool> animation;

					lock(_RoomLock)
					{
						if (WasDisposed)
							return;

						if (_participants.Count == 0)
						{
							if (MustDisposeIfEmpty)
							{
								Dispose();
								return;
							}

							foreach (var component in _components.Values)
							{
								component._componentLock.EnterWriteLock();
								try
								{
									if (!component._wasDisposed)
										component._changes.Clear();
								}
								finally
								{
									component._componentLock.ExitWriteLock();
								}
							}

							return;
						}

						animation = _animation;
						participants = _participants.ToArray();
						components = _components.Values.ToArray();
						addedComponents = _addedComponents.ToArray();
						removedComponents = _removedComponents.ToArray();

						_addedComponents.Clear();
						_removedComponents.Clear();
					}

					if (updateAnimation && animation != null && !animation.MoveNext())
					{
						Dispose();
						return;
					}

					var volatileValues = new List<KeyValuePair<long, object[]>>();
					var componentChanges = new List<KeyValuePair<RemoteGameComponent, KeyValuePair<int, object>[]>>();
					foreach (var component in components)
					{
						var changes = component._GetChanges();
						if (changes != null)
						{
							var pair = new KeyValuePair<RemoteGameComponent, KeyValuePair<int, object>[]>(component, changes);
							componentChanges.Add(pair);
						}

						component._AddVolatileValues(volatileValues);
					}

					// we must call StoreChanges even if there are none, as this allows some participants that had their notifications
					// disabled or with private components to work properly.
					foreach (var participant in participants)
						participant._StoreChanges(componentChanges, addedComponents, removedComponents, volatileValues);
				}
			#endregion

			#region ApplyChangesImmediately
				/// <summary>
				/// Applies any changes done to this room immediately.
				/// You must call this method if the room doesn't have an animation.
				/// </summary>
				public void ApplyChangesImmediately()
				{
					_ApplyChangesImmediately(false);
				}
			#endregion
			#region CreateComponent
				/// <summary>
				/// Creates a component into this room that is not owned by any participant and is public.
				/// </summary>
				public T CreateComponent<T>()
				where
					T: RemoteGameComponent
				{
					return _CreateComponent<T>(null);
				}
			#endregion
			#region GetComponents
				/// <summary>
				/// Gets all components in this room.
				/// Note that private components are only acessible by the Participant.GetPrivateComponents.
				/// </summary>
				/// <returns>An array with the components or null if this room was disposed.</returns>
				public RemoteGameComponent[] GetComponents()
				{
					lock(_RoomLock)
					{
						if (WasDisposed)
							return null;

						return _components.Values.ToArray();
					}
				}
			#endregion
			#region GetParticipants
				internal HashSet<RemoteGameParticipant> _participants = new HashSet<RemoteGameParticipant>();
				/// <summary>
				/// Gets all participants in this room.
				/// </summary>
				/// <returns>An array with the participants or null if this room was disposed.</returns>
				public RemoteGameParticipant[] GetParticipants()
				{
					lock(_RoomLock)
					{
						if (WasDisposed)
							return null;

						return _participants.ToArray();
					}
				}
			#endregion
			#region Start
				#region Start()
					/// <summary>
					/// Starts this run without an animation.
					/// Use it for rooms that respond to requests only.
					/// </summary>
					public void Start()
					{
						lock(_RoomLock)
						{
							CheckUndisposed();

							if (_started)
								throw new RemoteGameException("RemoteGameRoom already started.");

							_started = true;
						}

						ApplyChangesImmediately();
					}
				#endregion
				#region Start(double interval, IEnumerator<bool> animation)
					/// <summary>
					/// Starts this room, which will run the given animation in the given interval times.
					/// </summary>
					public void Start(double interval, IEnumerator<bool> animation)
					{
						if (animation == null)
							throw new ArgumentNullException("animation");

						lock(_RoomLock)
						{
							CheckUndisposed();

							if (_started)
								throw new RemoteGameException("RemoteGameRoom already started.");

							_started = true;
							_animation = animation;
							_timer = new HighPrecisionTimer(_ApplyChangesImmediately, interval);
						}
					}
				#endregion
			#endregion

			#region RegisterAction - Many overloads
				internal Dictionary<Type, Func<RemoteGameParticipant, RemoteGameRequest, object>> _registeredActions = new Dictionary<Type, Func<RemoteGameParticipant, RemoteGameRequest, object>>();

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<RemoteGameParticipant, T, object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action(participant, (T)request)
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action()
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<T, object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action((T)request)
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}

				/// <summary>
				/// Registers the action to be taken by a given request type.
				/// </summary>
				public IDisposable RegisterAction<T>(Func<RemoteGameParticipant, object> action)
				where
					T: RemoteGameRequest
				{
					if (action == null)
						throw new ArgumentNullException("action");

					var registeredAction = 
						new Func<RemoteGameParticipant, RemoteGameRequest, object>
						(
							(participant, request) => action(participant)
						);

					lock(_registeredActions)
						_registeredActions.Add(typeof(T), registeredAction);

					return new _UnregisterAction(this, typeof(T));
				}
			#endregion
		#endregion
	}
}
